﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mathhammer40K.Data;

namespace Mathhammer40K
{
    class CombatEngine
    {        
        CombatInputs combatInputs;
        CombatResults combatResults;

        public enum SpecialRules
        {
            None = 0,
            AP2=1,
            PowerWeapon=2,
            PowerFist=4,
            ThunderHammer=8,
            Poisoned=16,
            Charge=32,
            CounterAttack=64,
            TwoCCWeapons=128,
            Rage=256,
            FuriousCharge=512,
            FeelNoPain=1024,
            Stunned=2048,
            Overwatch=4096,
            Template=8192,
            HammerOfWrath = 16384,
            RerollHits = 32768,
            RerollWounds = 65536,
            PowerMaul = 131072,
            PowerAxe = 262144,
            Dummy5 = 524288,
            Dummy6 = 1048576
            
            
            
        }

        public CombatResults ComputeBattleDistribution(CombatInputs inputs = null, CombatResults results = null, Int32 count = 1, bool assault = true, int side = 1)
        {
            if (results != null)
                combatResults = results;
            for(int i = 0; i < count; i++)
            {
                // compute a battle                
                ComputeBattle(inputs, combatResults, assault, side);
            }

            // Calculate distributions
            combatResults.CombatDistribution.Clear();

            var query = from combat in combatResults.Results
                        group combat by new { combat.Name, combat.UnitID, combat.Initiative } into unitCombatGroup
                        orderby unitCombatGroup.Key.Initiative descending
                        select unitCombatGroup;

            CombatResults.CombatDistributionRow distributionRow = null;
            foreach (var combatGroup in query)
            {
                CombatResults.ResultsRow combat = null;
                foreach (CombatResults.ResultsRow row in combatGroup)
                {
                    combat = row;
                    break; // we only need the first one
                }
                                                   
                
                if (distributionRow != null)
                    combatResults.CombatDistribution.AddCombatDistributionRow(distributionRow); // add row
                distributionRow = combatResults.CombatDistribution.NewCombatDistributionRow(); // make new row
                

                distributionRow.Name = combat.Name;
                distributionRow.Side = combat.Side;

                ////////////////////////////////////////////////
                // Attacks
                IEnumerable<Double> valueResult = from unitCombat in combatResults.Results
                                                  where unitCombat.Name == combat.Name && unitCombat.Side == combat.Side
                                                  select (Double)unitCombat.Attacks;

                // Statistics!
                MathNet.Numerics.Statistics.DescriptiveStatistics stats = new MathNet.Numerics.Statistics.DescriptiveStatistics(valueResult);

                distributionRow.AttacksMin = (Int32)stats.Minimum;
                distributionRow.AttacksMax = (Int32)stats.Maximum;
                distributionRow.AttacksAvg = stats.Mean;
                distributionRow.AttacksStdDev = stats.StandardDeviation;

                ////////////////////////////////////////////////
                // Hits
                valueResult = from unitCombat in combatResults.Results
                              where unitCombat.Name == combat.Name && unitCombat.Side == combat.Side
                              select (Double)unitCombat.Hits;

                // Statistics!
                stats = new MathNet.Numerics.Statistics.DescriptiveStatistics(valueResult);

                distributionRow.HitsMin = (Int32)stats.Minimum;
                distributionRow.HitsMax = (Int32)stats.Maximum;
                distributionRow.HitsAvg = stats.Mean;
                distributionRow.HitsStdDev = stats.StandardDeviation;

                ////////////////////////////////////////////////
                // Wounds
                valueResult = from unitCombat in combatResults.Results
                              where unitCombat.Name == combat.Name && unitCombat.Side == combat.Side
                              select (Double)unitCombat.Wounds;

                // Statistics!
                stats = new MathNet.Numerics.Statistics.DescriptiveStatistics(valueResult);

                distributionRow.WoundsMin = (Int32)stats.Minimum;
                distributionRow.WoundsMax = (Int32)stats.Maximum;
                distributionRow.WoundsAvg = stats.Mean;
                distributionRow.WoundsStdDev = stats.StandardDeviation;

                ////////////////////////////////////////////////
                // Killed
                valueResult = from unitCombat in combatResults.Results
                              where unitCombat.Name == combat.Name && unitCombat.Side == combat.Side
                              select (Double)unitCombat.Killed;

                // Statistics!
                stats = new MathNet.Numerics.Statistics.DescriptiveStatistics(valueResult);

                distributionRow.KilledMin = (Int32)stats.Minimum;
                distributionRow.KilledMax = (Int32)stats.Maximum;
                distributionRow.KilledAvg = stats.Mean;
                distributionRow.KilledStdDev = stats.StandardDeviation;

                

            }

            // add last row
            if (distributionRow != null)
                combatResults.CombatDistribution.AddCombatDistributionRow(distributionRow);

            return combatResults;
        }

        public CombatResults ComputeBattle(CombatInputs inputs = null, CombatResults results = null, bool assault = true, int side = 1)
        {
            if (inputs != null)
                combatInputs = (CombatInputs)inputs.Copy();
            if (results != null)
                combatResults = results;

            if (assault)
                FightAssault();
            else
                DoShooting(side);

            return combatResults;
        }

        private void FightAssault()
        {
            ImperialRoller roller = new ImperialRoller();
            ResolveOverwatch(roller);

            // Modify initiatives as needed
            List<CombatInputs.UnitsRow> hammerOfWrathRows = new List<CombatInputs.UnitsRow>();
            foreach (CombatInputs.UnitsRow row in combatInputs.Units)
            {
                if ((row.SpecialRules & (int)SpecialRules.HammerOfWrath) != 0)
                {
                    if ((row.SpecialRules & (int)SpecialRules.Charge) != 0) // only on the charge
                    {
                        CombatInputs.UnitsRow HOWrow = combatInputs.Units.NewUnitsRow();
                        HOWrow.Init(row);
                        HOWrow.Name = HOWrow.Name + " Hammer of Wrath";
                        HOWrow.Initiative = 10;
                        HOWrow.Attacks = 1;
                        HOWrow.SpecialRules = ((int)SpecialRules.HammerOfWrath | (int)SpecialRules.Charge);

                        hammerOfWrathRows.Add(HOWrow);

                        // take away hammer of wrath from the original unit
                        row.SpecialRules = row.SpecialRules ^ (int)SpecialRules.HammerOfWrath;
                    }
                }

                if ((row.SpecialRules & (int)SpecialRules.PowerFist) != 0)
                    row.Initiative = 1;
                if ((row.SpecialRules & (int)SpecialRules.PowerAxe) != 0)
                    row.Initiative = 1;
                if ((row.SpecialRules & (int)SpecialRules.Stunned) != 0)
                    row.Initiative = 1;
            }

            foreach (CombatInputs.UnitsRow row in hammerOfWrathRows)
                combatInputs.Units.AddUnitsRow(row);


            // Sort by Initiative
            List<CombatResults.ResultsRow> outputs = new List<CombatResults.ResultsRow>(combatInputs.Units.Count);

            var query = from unit in combatInputs.Units.AsEnumerable()
                        orderby unit.Initiative descending
                        select unit;

            Int32 initiativeLevel = Int32.MaxValue;
            foreach (CombatInputs.UnitsRow unit in query)
            {
                if (initiativeLevel != unit.Initiative)
                {
                    // we've moved on to the next stage of combat - clean up
                    initiativeLevel = unit.Initiative;

                    foreach (CombatInputs.UnitsRow row in combatInputs.Units)
                    {
                        row.Count -= row.ToRemove;
                        row.Count = Math.Max(0, row.Count); // min is 0;
                        row.ToRemove = 0;
                    }

                }
                // Results
                CombatResults.ResultsRow res = combatResults.Results.NewResultsRow();
                res.Init(unit);

                // calculate attacks

                Int32 attacksPerUnit = unit.Attacks;

                ////////////////////////////////////////
                // Special rules
                bool twoCCweapons = (unit.SpecialRules & (int)SpecialRules.TwoCCWeapons) != 0;
                bool charge = (unit.SpecialRules & (int)SpecialRules.Charge) != 0;
                bool rage = charge && ((unit.SpecialRules & (int)SpecialRules.Rage) != 0);
                bool counterattack = !charge && ((unit.SpecialRules & (int)SpecialRules.CounterAttack) != 0);
                bool hammerOfWrath = charge && ((unit.SpecialRules & (int)SpecialRules.HammerOfWrath) != 0);
                
                if (!hammerOfWrath)
                {
                    attacksPerUnit += (twoCCweapons ? 1 : 0)
                        + (charge ? 1 : 0)
                        + (rage ? 1 : 0)
                        + (counterattack ? 1 : 0);
                }


                res.Attacks = attacksPerUnit * unit.Count;

                ////////////////////////////////////////
                // Calculate Weapon Skills
                Int32 unitWeaponSkill = CalculateMajoritySkill(combatInputs.Units.WeaponSkillColumn, unit.Side, false);
                Int32 enemyWeaponSkil = CalculateMajoritySkill(combatInputs.Units.WeaponSkillColumn, unit.Side, true);
                System.Console.WriteLine(String.Format("Unit WS: {0} Enemy WS: {1}", unitWeaponSkill, enemyWeaponSkil));

                ////////////////////////////////////////
                // Calculate hits - Assault to hit Table

                bool reroll = (unit.SpecialRules & (int)SpecialRules.RerollHits) != 0;
                ImperialRoller.Distribution rollResult = roller.RollD6(res.Attacks);
                Int32 hits = 0;

                if (!hammerOfWrath)
                {
                    if (unitWeaponSkill > enemyWeaponSkil) // Hit on 3s
                    {
                        hits = rollResult.ThreePlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos);
                            hits += rollResult.ThreePlus;
                        }
                    }
                    else if (unitWeaponSkill <= enemyWeaponSkil && !(2 * unitWeaponSkill <= enemyWeaponSkil)) // enemyAttack/2 < unitAttack <= enemyAttack - hit on 4s
                    {
                        hits = rollResult.FourPlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes);
                            hits += rollResult.FourPlus;
                        }
                    }
                    else if (unitWeaponSkill != 0)
                    {
                        hits = rollResult.FivePlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes + rollResult.Fours);
                            hits += rollResult.FivePlus;
                        }
                    }
                }
                else
                {
                    // Hammer of wrath auto-hits
                    hits = res.Attacks;
                }

                res.Hits = hits;

                Int32 unitStrength = unit.Strength;               
                bool furiousCharge = charge && ((unit.SpecialRules & (int)SpecialRules.FuriousCharge) != 0);
                bool powerFist = (unit.SpecialRules & (int)SpecialRules.PowerFist) != 0;
                bool powerMaul = (unit.SpecialRules & (int)SpecialRules.PowerMaul) != 0;
                bool powerAxe = (unit.SpecialRules & (int)SpecialRules.PowerAxe) != 0;

                if (!hammerOfWrath) // Hammer of wrath uses unmodified strength
                {
                    unitStrength *= powerFist ? 2 : 1; // powerfist modifier
                    unitStrength += furiousCharge ? 1 : 0; // furious charge modifier
                    unitStrength += powerMaul ? 2 : 0; // Power Maul = +2 strength
                    unitStrength += powerAxe ? 1 : 0; // Power Axe = +1 strength
                    unitStrength = Math.Min(unitStrength, 10); // Max 10
                }
                res.Wounds = CalculateWounds(unit, res.Hits, unitStrength, roller);

                //////////////////////////////////////////////
                // AP
                Int32 AP = Int32.MaxValue; // NO AP
                if ((unit.SpecialRules & (int)SpecialRules.PowerWeapon) != 0)
                    AP = 3;
                if ((unit.SpecialRules & (int)SpecialRules.PowerFist) != 0)
                    AP = 2;
                if ((unit.SpecialRules & (int)SpecialRules.PowerAxe) != 0)
                    AP = 2;
                if ((unit.SpecialRules & (int)SpecialRules.PowerMaul) != 0)
                    AP = 4;
                if ((unit.SpecialRules & (int)SpecialRules.AP2) != 0)
                    AP = 2;



                res.Killed = CalculateKilled(unit, AP, res.Wounds, unitStrength, roller);



                combatResults.Results.AddResultsRow(res);
            }
        }

        private int CalculateWounds(CombatInputs.UnitsRow unit, int hits, int unitStrength, ImperialRoller roller)
        {
            ////////////////////////////////////////
            // Calculate Wounds
            
            Int32 enemyToughness = CalculateMajoritySkill(combatInputs.Units.ToughnessColumn, unit.Side, true);

            bool reroll = ((unit.SpecialRules & (int)SpecialRules.RerollWounds) != 0);

            ImperialRoller.Distribution rollResult = roller.RollD6(hits);
            Int32 wounds = 0;
            // Wound Table
            Int32 diff = unitStrength - enemyToughness;
            if (diff >= 2) // e.g. S5 vs T3 - wound on 2s
            {
                wounds = rollResult.TwoPlus;
                if (reroll)
                {
                    rollResult = roller.RollD6(rollResult.Ones);
                    wounds += rollResult.TwoPlus;
                }
            }
            else
            {
                switch (diff)
                {
                    case 1: // e.g. S4 vs T3 - wound on 3s
                        wounds = rollResult.ThreePlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos);
                            wounds += rollResult.ThreePlus;
                        }
                        break;
                    case 0: // even - wound on 4s
                        wounds = rollResult.FourPlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes);
                            wounds += rollResult.FourPlus;
                        }
                        break;
                    case -1: // e.g. S3 vs T4 - wound on 5s
                        wounds = rollResult.FivePlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes + rollResult.Fours);
                            wounds += rollResult.FivePlus;
                        }
                        break;
                    case -2: // eg. S4 vs T6 - wound on 6s
                        wounds = rollResult.SixPlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes + rollResult.Fours + rollResult.Fives);
                            wounds += rollResult.SixPlus;
                        }
                        break;
                    case -3: // S4 vs T7 - wound on 6s
                        wounds = rollResult.SixPlus;
                        if (reroll)
                        {
                            rollResult = roller.RollD6(rollResult.Ones + rollResult.Twos + rollResult.Threes + rollResult.Fours + rollResult.Fives);
                            wounds += rollResult.SixPlus;
                        }
                        break;
                    default:
                        wounds = 0; // can't wound at all
                        break;
                }
            }

            return wounds;
        }


        private int CalculateKilled(CombatInputs.UnitsRow unit, int AP, int wounds, int unitStrength, ImperialRoller roller)
        {
            ImperialRoller.Distribution rollResult;
            //////////////////////////////////////////////
            // Saves
            var saveQuery = from enemy in combatInputs.Units
                            where enemy.Side != unit.Side
                            orderby enemy.ID ascending
                            select enemy;

            Int32 kills = 0;
            // loop through the units in the order they were entered, taking their best save against the wounds until all are dead or wounds are exhausted
            foreach (CombatInputs.UnitsRow enemy in saveQuery)
            {
                if (wounds <= 0)
                    break; // allocated all wounds
                Int32 bestSave = Int32.MaxValue; // no save
                if (!enemy.IsArmorSaveNull() && enemy.ArmorSave < bestSave)
                    bestSave = enemy.ArmorSave;

                // Check AP for power weapons
                if (AP <= bestSave) // negated save
                    bestSave = Int32.MaxValue;

                if (!enemy.IsInvulnerableSaveNull() && (enemy.InvulnerableSave < bestSave))
                    bestSave = enemy.InvulnerableSave;


                Int32 modelWounds = enemy.Wounds;
                while (wounds > 0 && enemy.Count > enemy.ToRemove)
                {
                    wounds--;

                    // Roll it!
                    rollResult = roller.RollD6();

                    bool unsaved = true;
                    switch (bestSave)
                    {
                        case 2:
                            unsaved = rollResult.TwoPlus == 0;
                            break;
                        case 3:
                            unsaved = rollResult.ThreePlus == 0;
                            break;
                        case 4:
                            unsaved = rollResult.FourPlus == 0;
                            break;
                        case 5:
                            unsaved = rollResult.FivePlus == 0;
                            break;
                        case 6:
                            unsaved = rollResult.SixPlus == 0;
                            break;
                        default:
                            // no save
                            break;
                    }

                    int enemyToughness = enemy.Toughness;

                    if (unsaved) // unsaved wound
                    {
                        // Feel no pain
                        if ((enemy.SpecialRules & (int)SpecialRules.FeelNoPain) != 0)
                        {
                            // Instant death
                            if (unitStrength >= (enemyToughness * 2))
                                break;

                            ImperialRoller.Distribution feelNoPainRoll = roller.RollD6();
                            if (feelNoPainRoll.FivePlus != 0)
                                continue; // ignore the unsaved wound
                        }

                        // Check for instant death
                        if (unitStrength >= (enemyToughness * 2))
                        {
                            enemy.ToRemove++; // remove a model
                            kills++;
                        }
                        else
                        {
                            // Take a wound
                            modelWounds--;
                            if (modelWounds <= 0)
                            {
                                enemy.ToRemove++; // remove a model
                                modelWounds = enemy.Wounds;
                                kills++;
                            }
                        }
                    }
                }
            }

            return kills;
        }

        private void ResolveOverwatch(ImperialRoller roller)
        {
            // Any models not on the charge can fire overwatch            

            ImperialRoller.Distribution rollResult;

            foreach (CombatInputs.UnitsRow unit in combatInputs.Units)
            {
                if ((unit.SpecialRules & (int)SpecialRules.Charge) == 0) // can't fire overwatch on the charge
                {
                    if ((unit.SpecialRules & (int)SpecialRules.Overwatch) != 0)
                    {
                        CombatResults.ResultsRow res = combatResults.Results.NewResultsRow();
                        res.Init(unit);
                        res.Name += " Overwatch";
                        res.Initiative = 11;

                        int attacks = 0;
                        int hits = 0;
                        int wounds = 0;
                        int killed = 0;
                        // Calculate hits (Weapon to hit table)
                        if ((unit.SpecialRules & (int)SpecialRules.Template) != 0)
                        {
                            // Template weapon gets D3 Hits
                            rollResult = roller.RollD3(unit.Count);
                            hits = rollResult.Ones + 2 * rollResult.Twos + 3 * rollResult.Threes;
                            attacks = hits;
                        }
                        else
                        {
                            // hit on sixes
                            attacks = unit.Count * unit.WeaponShots;
                            rollResult = roller.RollD6(attacks);

                            hits = rollResult.SixPlus;

                        }

                        // Calculate wounds
                        wounds = CalculateWounds(unit, hits, unit.WeaponStrength, roller);

                        killed = CalculateKilled(unit, unit.WeaponAP, wounds, unit.WeaponStrength, roller);

                        res.Attacks = attacks;
                        res.Hits = hits;
                        res.Wounds = wounds;
                        res.Killed = killed;
                        combatResults.Results.AddResultsRow(res);
                    }
                }
            }




        }


        private void DoShooting(int side = 1)
        {
            // Any models not on the charge can fire overwatch            

            ImperialRoller roller = new ImperialRoller();
            ImperialRoller.Distribution rollResult;

            var query = from unit in combatInputs.Units
                        where unit.Side == side
                        select unit;

            foreach (CombatInputs.UnitsRow unit in query)
            {                    
                CombatResults.ResultsRow res = combatResults.Results.NewResultsRow();
                res.Init(unit);                

                int attacks = 0;
                int hits = 0;
                int wounds = 0;
                int killed = 0;
                // Calculate hits (Weapon to hit table)
                if ((unit.SpecialRules & (int)SpecialRules.Template) != 0)
                {
                    // Template weapon gets D3 Hits
                    rollResult = roller.RollD3(unit.Count);
                    hits = rollResult.Ones + 2 * rollResult.Twos + 3 * rollResult.Threes;
                    attacks = hits;
                }
                else
                {
                    // hit on 7-BS
                    attacks = unit.Count * unit.WeaponShots;
                    rollResult = roller.RollD6(attacks);

                    switch (unit.BallisticSkill)
                    {
                        case 1:
                            hits = rollResult.SixPlus;
                            break;
                        case 2:
                            hits = rollResult.FivePlus;
                            break;
                        case 3:
                            hits = rollResult.FourPlus;
                            break;
                        case 4:
                            hits = rollResult.ThreePlus;
                            break;
                        case 5:
                            hits = rollResult.TwoPlus;
                            break;
                        // to do: resolve higher ballistic skill
                        case 6:
                            hits = rollResult.TwoPlus;
                            break;                            
                        case 7:
                            hits = rollResult.SixPlus;
                            break;
                        case 8:
                            hits = rollResult.SixPlus;
                            break;
                        default:
                            break;
                    }
                }

                // Calculate wounds
                wounds = CalculateWounds(unit, hits, unit.WeaponStrength, roller);

                killed = CalculateKilled(unit, unit.WeaponAP, wounds, unit.WeaponStrength, roller);

                res.Attacks = attacks;
                res.Hits = hits;
                res.Wounds = wounds;
                res.Killed = killed;
                combatResults.Results.AddResultsRow(res);
                    
                
            }




        }

        public Int32 CalculateMajoritySkill(System.Data.DataColumn column, Int32 side, bool enemy)
        {
            Int32 returnValue = 0;

            var query = from unit in combatInputs.Units
                        where !unit.IsNameNull() && (!enemy && unit.Side == side) || (enemy && unit.Side != side)
                        orderby column ascending
                        select unit;

            Int32 modelsAtMaxAttribute = 0;
            Int32 currentValue = 0;
            Int32 currentModelsAtValue = 0;

            foreach (CombatInputs.UnitsRow unit in query)
            {
                // If we've movel on to another weapon skill, reset the count
                if (unit.WeaponSkill > currentValue)
                {
                    currentValue = unit.WeaponSkill;
                    currentModelsAtValue = 0;
                }

                // Count all of the models at the current weapon skill
                currentModelsAtValue += unit.Count;

                if (currentModelsAtValue > modelsAtMaxAttribute) // we've got more at this weapon skill than at the previous max
                {
                    // new weapon skill max
                    modelsAtMaxAttribute = currentModelsAtValue;
                    returnValue = currentValue;
                }
            }

            return returnValue;
        }

    }
}
